﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shoozla.DataModel.Helper;
using System.Windows.Media.Imaging;



namespace Shoozla.DataModel
{
    public enum SearchMethod
    { 
        Album,
        Song,
        Artist,
        All, //album + song + artist
    }

    /// <summary>
    /// Add-in to search for covers
    /// </summary>
    public abstract class CoverSearcher : IProgressOperation
    {

        //IProgressOperation
        public event EventHandler<ProgressOperationEventArgs> ProgressChanged;
        public event EventHandler<ProgressOperationEventArgs> Completed;
        public event EventHandler<ProgressOperationEventArgs> Started;
        public int Minimum { get; set; }
        public int Maximum { get; set; } //do not call set
        public int Current { get; set; }
        public String Description { get; set; }

        //caches
        public ConcurrentDictionary<string, Uri> AlbumUriCache { get; private set; }
        public ConcurrentDictionary<string, Uri> ArtistUriCache { get; private set; }
        public ConcurrentDictionary<string, Uri> SongUriCache { get; private set; }

        //public information
        public String AddInName { get; protected set; }
        public String AddInDescription { get; protected set; }

        private List<Mp3File> _externalMp3List;
        public ICoverStorage CoverImageCache { get; private set; }
        private ParallelOptions _parallelSettings;
        private CancellationTokenSource _cancelTokenSource;
        public CancellationTokenSource CancelTokenSource {
            get { return _cancelTokenSource; } 
            set {
                if (value != _cancelTokenSource && value != null)
                {
                    if (value.Token.IsCancellationRequested)
                        throw new Exception("Cannot set CancelToken");

                    _cancelTokenSource = value;
                    _parallelSettings.CancellationToken = value.Token;
                }
            }
        }   

        //events


        public event CoverFoundHandler CoverFound;
        public event UserInteraction NotifyUser;
        public delegate void CoverFoundHandler (string displayName, SearchMethod method, Uri urlFound);
        public delegate bool UserInteraction(string message, bool canCancel); //ask to user o notify

        public ThreadingFactorEnum ThreadingFactor { get; set; }
        public List<Mp3File> Songs { get; set; }
        private StatisticsLogger _stats;

        public CoverSearcher()
        {
            SetAddInInformation();

            AlbumUriCache = new ConcurrentDictionary<string, Uri>();
            ArtistUriCache = new ConcurrentDictionary<string, Uri>();
            SongUriCache = new ConcurrentDictionary<string, Uri>();
            Completed += new EventHandler<ProgressOperationEventArgs>(FinalizeSearchOnCompleted);
            _parallelSettings = new ParallelOptions();
            ThreadingFactor = ThreadingFactorEnum.Maximum;
        }

        public CoverSearcher(ICoverStorage coverCache, CancellationTokenSource cancelTokenSource, StatisticsLogger stats)
            : this()
        {
            _stats = stats;
            CoverImageCache = coverCache;
            CancelTokenSource = cancelTokenSource;
            
        }

        public override string ToString()
        {
            return AddInName;
        }

        protected virtual void SetAddInInformation()
        {
            AddInName = "Generic Cover Searcher";
            AddInDescription = "Add-in Description";
        }

        protected bool AskToUser(string message)
        {
            if (NotifyUser == null)
                return false;

            return NotifyUser.Invoke(message, true);
        }

        protected void NotifyToUser(string message)
        {
            if (NotifyUser != null)
                NotifyUser.Invoke(message, false);
        }

        public BitmapImage Icon
        {
            get { return ImageHelper.ImageToBitmapImage(GetIcon()); } 
        }

        public virtual Image GetIcon()
        {
            return null;
        }

        /// <summary>
        /// To call before to search
        /// </summary>
        /// <returns>false if the initialization fails</returns>
        public virtual bool Initialize() { return true; }

        public void CancelAsync() { CancelTokenSource.Cancel(); StopSearch(); }

        public void StopSearch()
        {
            Trace.WriteLine("Search stopped");

            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) =>
            {
                return
                    !String.IsNullOrEmpty(m.AlbumName) &&
                    m.ImageUrl == null;
            }))
            {
                string indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (AlbumUriCache.ContainsKey(indexName))
                {
                    Uri coverUri = AlbumUriCache[indexName];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                String indexKey = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                if (SongUriCache.ContainsKey(indexKey))
                {
                    Uri coverUri = SongUriCache[indexKey];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                if (m.Artist != null)
                {
                    if (ArtistUriCache.ContainsKey(m.Artist))
                    {
                        Uri coverUri = ArtistUriCache[m.Artist];
                        if (coverUri != null)
                        {
                            m.ImageUrl = coverUri;
                        }
                    }
                }
            }

            RaiseSearchCompleted(Current);
        }

        public void ClearCache()
        {
            AlbumUriCache.Clear();
            SongUriCache.Clear();
            ArtistUriCache.Clear();
        }

        /// <summary>
        /// Delete all empty covers, covers not found, to avoid to search for them every run
        /// </summary>
        void FinalizeSearchOnCompleted(object sender, ProgressOperationEventArgs e)
        {
            //i need to delete all empty covers, covers not found, to avoid to search for them every run
            Uri dummyUrl;

            //delete albums
            var emptyItem = (from c in AlbumUriCache
                             where c.Value == null
                             select c);
            foreach (var i in emptyItem)
                AlbumUriCache.TryRemove(i.Key, out dummyUrl);

            //delete songs
            emptyItem = (from c in SongUriCache
                             where c.Value == null
                             select c);
            foreach (var i in emptyItem)
                SongUriCache.TryRemove(i.Key, out dummyUrl);

            //delete artist
            emptyItem = (from c in ArtistUriCache
                         where c.Value == null
                         select c);
            foreach (var i in emptyItem)
                ArtistUriCache.TryRemove(i.Key, out dummyUrl);

            int albumFound = AlbumUriCache.Count((v) => { return v.Value != null; });
            int artistFound = ArtistUriCache.Count((v) => { return v.Value != null; });
            int songFound = SongUriCache.Count((v) => { return v.Value != null; });
            int totalFileProcessed = Songs.Count;
            int totalFileMatched = Songs.Count<Mp3File>((c) => c.ImageUrl != null);

            _stats.TotalSearchedFiles = totalFileProcessed;
            _stats.AlbumFound = albumFound;
            _stats.ArtistFound = artistFound;
            _stats.SongFound = songFound;
            _stats.FileMatched = totalFileMatched;

            Trace.WriteLine(String.Format("[INFO] Search Terminated"));
        }

        /// <summary>
        /// Syncronous method to find a single cover for an Mp3.
        /// It store the cover in a image cache (CoverImageCache)
        /// </summary>
        /// <param name="song"></param>
        /// <returns>The URI of the cover, or null in negative case.</returns>
        public Uri FindSingleCover(Mp3File song)
        {
            if (song == null || song.Artist == null || song.Title == null)
                return null;

            if (!Initialize()) //if needed ask to the user to connect to the website
                return null;

            Uri cover = null;

            if (!String.IsNullOrEmpty(song.AlbumName) && !String.IsNullOrEmpty(song.Artist))
                cover = FindByAlbumName(song);

            if (cover == null && !String.IsNullOrEmpty(song.Title))
                cover = FindBySongTitle(song);

            if (cover == null)
                cover = FindArtist(song);

            if (cover != null)
                CoverImageCache.AddCoverToStorage(cover.AbsoluteUri, song.Artist + " " + song.Title, SearchMethod.All);

            return cover;
        }

        protected void RaiseProgressMade()
        {
            Current++;

            if (ProgressChanged != null)
                foreach (Delegate d in ProgressChanged.GetInvocationList())
                {
                    d.DynamicInvoke(new object[] { this, new ProgressOperationEventArgs(Current, Maximum, Description) });
                }
        }

        protected void RaiseCoverFound(string displayName, SearchMethod method, Uri urlFound)
        {
            Trace.WriteLine(String.Format("[COVER] Found {0}: {1} ", method, displayName));
          
            if (CoverFound != null)
                foreach (Delegate d in CoverFound.GetInvocationList())
                {
                    d.DynamicInvoke(new object[] { displayName,method,urlFound});
                }
        }

        private void RaiseSearchStarted(int count)
        {
            Description = "Searching "+ count + " albums";
            Maximum = count;
            Current = 0;

            if (Started != null)
                foreach (Delegate d in Started.GetInvocationList())
                {
                    d.DynamicInvoke(new object[] { null, new ProgressOperationEventArgs(0, Maximum, Description) });
                }
        }

        private void RaiseSearchCompleted(int found)
        {
            Description = "Search for artwork completed";
            if (Completed != null)
                foreach (Delegate d in Completed.GetInvocationList())
                {
                    d.DynamicInvoke(new object[] { this, new ProgressOperationEventArgs(found, Maximum, Description) });
                }

        }
       

        #region Thread workers
        //Finds all the covers for the album in the list
        private void AlbumCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            String[] names = Helper.StringHelper.GetSeparatedNames(item.Key);
            String description = names[0] + " (" + names[1] + ")";
            if (coverUri == null)               
                coverUri = FindByAlbumName(names[0], names[1]);
            else
                RaiseCoverFound(description, SearchMethod.Album, coverUri);

            AlbumUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri, description, SearchMethod.Album);
            
        }

        private void SongCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            String[] names = Helper.StringHelper.GetSeparatedNames(item.Key);
            String description = names[0] + " - " + names[1];
            if (coverUri == null)
                coverUri = FindBySongTitle(names[0], names[1]);
            else
                RaiseCoverFound(description, SearchMethod.Song, coverUri);
            
            SongUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri, description, SearchMethod.Song);

        }

        private void ArtistCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            if (coverUri == null)
                coverUri = FindArtist(item.Key);
            else
                RaiseCoverFound(item.Key, SearchMethod.Artist, coverUri);

            ArtistUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri, item.Key,SearchMethod.Artist);
        }

        #endregion


        //this function minimize all the trips to the website, looping in the memory lists (that's is faster)
        /// <summary>
        /// Find all the covers for the given Mp3 list, multithread synchronous function.
        /// All the covers found (Uri) are saved in a cache (CoverImageCache) and downloaded immediately
        /// Before to call the Songs property must be initialized with the Mp3 list
        /// Asynchronous call
        /// </summary>
        public void Start()
        {
            if (Songs == null || Songs.Count <= 0)
                throw new ArgumentNullException("Song list is empty");

            if(_parallelSettings.CancellationToken.IsCancellationRequested)
                throw new Exception("Cancel Token already signaled");
            try
            {
                Task.Factory.StartNew((Action)(() => FindMultipleCovers()),TaskCreationOptions.LongRunning);
            }
            catch
            {
                StopSearch();
            }

        }

        /// <summary>
        /// Synchronous call
        /// </summary>
        private void FindMultipleCovers()
        {       

            _externalMp3List = Songs;
            _parallelSettings.MaxDegreeOfParallelism = ThreadHelper.CalculateTreads(Songs.Count, ThreadingFactor);

            foreach (Mp3File m in _externalMp3List)
                m.ImageUrl = null; //needed when i search the second time for the same song

            //analyze all the albums name, create a list "to download", 1 entry for each album --> no duplicates
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return !String.IsNullOrEmpty(m.AlbumName); }))
            {
                //only song with album name
                String indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (!AlbumUriCache.ContainsKey(indexName))
                    AlbumUriCache.AddOrUpdate(indexName, (Uri)null, (k, v) => null);
            }

            RaiseSearchStarted(AlbumUriCache.Count);

            try
            {                             
                Parallel.ForEach(AlbumUriCache, _parallelSettings, list => AlbumCoverSearcherWorker(list));
            }
            catch (OperationCanceledException ae)
            {
                StopSearch();
                return; //search cancelled
            }

            AlbumSearchCompleted();
        }
    
        #region callbacks

        //called when all album have been found
        void AlbumSearchCompleted()
        {
            //assigning the cover URI to each song
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return !String.IsNullOrEmpty(m.AlbumName); }))
            {
                string indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (AlbumUriCache.ContainsKey(indexName))
                {
                    Uri coverUri = AlbumUriCache[indexName];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }
            

            //now i have all the album found! check which song needs more work
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                //only song without image url
                if (!String.IsNullOrEmpty(m.Artist) && !String.IsNullOrEmpty(m.Title))
                {
                    String indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                    if (!SongUriCache.ContainsKey(indexName))
                        SongUriCache.AddOrUpdate(indexName, (Uri)null, (k, v) => null);
                }
            }

            Maximum = SongUriCache.Count; //extending the progress operation limit
            Description = "Searching " + Maximum + " tracks";
            Current = 0;

            try
            {

                Parallel.ForEach(SongUriCache, _parallelSettings, list => SongCoverSearcherWorker(list));

            }
            catch (OperationCanceledException ae)
            {
                StopSearch();
                return; //search cancelled
            }

            SongsSearchCompleted();
        }

        //called when all songs titles have been downloaded
        void SongsSearchCompleted()
        {
            //assign the new cover to the related song
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                String indexKey = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                if (SongUriCache.ContainsKey(indexKey))
                {
                    Uri coverUri = SongUriCache[indexKey];
                    if (coverUri != null)
                    {
                       // CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //now i have all the album+songs found! check which song needs more work (artist)
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                //only song without image url
                if (!String.IsNullOrEmpty(m.Artist))
                    if (!ArtistUriCache.ContainsKey(m.Artist))
                        ArtistUriCache.AddOrUpdate(m.Artist, (Uri)null, (k, v) => null);
            }

            Maximum = ArtistUriCache.Count; //extending the progress operation limit
            Description = "Searching " + Maximum + " artists";
            Current = 0;

            //continue with searching

            try
            {
                Parallel.ForEach(ArtistUriCache, _parallelSettings, list => ArtistCoverSearcherWorker(list));
            }
            catch (OperationCanceledException ae)
            {
                StopSearch();
                return;
            } 

            ArtistSearchCompleted();

        }

        //called when all artist have been found
        void ArtistSearchCompleted()
        {
            //assign artists
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                if (m.Artist != null)
                {
                    if (ArtistUriCache.ContainsKey(m.Artist))
                    {
                        Uri coverUri = ArtistUriCache[m.Artist];
                        if (coverUri != null)
                        {
                         //   CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
                            m.ImageUrl = coverUri;                          
                        }
                    }
                }
            }
           
            //done, all the songs with a cover have a ImageUri value != null --> the image is downloaded in parallel
            RaiseSearchCompleted(Current);
        }

        #endregion

        #region Find methods

        #region Album
        public Uri FindByAlbumName(Mp3File song)
        {
            return FindByAlbumName(song.Artist, song.AlbumName);
        }
        public Uri FindByAlbumName(string artist, string albumName)
        {
            Uri ret = FindByAlbumNameInternal(artist.ToLowerInvariant(), albumName.ToLowerInvariant());
            if (ret != null)
            {
                RaiseProgressMade(); //to track the overall progress
                RaiseCoverFound(albumName + " (" + artist + ")", SearchMethod.Album, ret);
            }
            return ret;
        }
        public virtual Uri FindByAlbumNameInternal(String artist, String albumName) { return null; }
        #endregion

        #region Title
        public Uri FindBySongTitle(Mp3File song)
        {
           return FindBySongTitle(song.Artist, song.Title);
        }
        public Uri FindBySongTitle(string artist, string title)
        {
            Uri ret = FindBySongTitleInternal(artist.ToLowerInvariant(), title.ToLowerInvariant());
            if (ret != null)
            {
                RaiseProgressMade(); //to track the overall progress
                RaiseCoverFound(artist + " - " + title, SearchMethod.Song, ret);
            }
            return ret;
        }
        public virtual Uri FindBySongTitleInternal(String artist, String title) { return null; }
        #endregion

        #region Artist
        public Uri FindArtist(Mp3File song)
        {
            return FindArtist(song.Artist.ToLowerInvariant());
        }
        public Uri FindArtist(string artist)
        {
            Uri ret = FindArtistInternal(artist);
            if (ret != null)
            {
                RaiseProgressMade(); //to track the overall progress
                RaiseCoverFound(artist, SearchMethod.Artist, ret);
            }
            return ret;
           
        }
        public virtual Uri FindArtistInternal(String artist) { return null; }
        #endregion

        #endregion

    }
}
